/*
 * Copyright 2012 Marsrutai.lt
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package lt.marsrutai.mobile.android.service.impl;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import lt.marsrutai.mobile.android.dao.DataDao;
import lt.marsrutai.mobile.android.dao.dto.ArrivalTimeDto;
import lt.marsrutai.mobile.android.dao.dto.BusStopDto;
import lt.marsrutai.mobile.android.dao.dto.BusTrackDto;
import lt.marsrutai.mobile.android.dao.dto.DaysOfWeekDto;
import lt.marsrutai.mobile.android.dao.dto.GeoPointDto;
import lt.marsrutai.mobile.android.dao.dto.RouteDto;
import lt.marsrutai.mobile.android.dao.dto.ScheduleDto;
import lt.marsrutai.mobile.android.dao.dto.ScopeInfoDto;
import lt.marsrutai.mobile.android.dao.dto.SearchResultDto;
import lt.marsrutai.mobile.android.dao.dto.SegmentDto;
import lt.marsrutai.mobile.android.dao.dto.StopDayTimesDto;
import lt.marsrutai.mobile.android.dao.dto.TransportDto;
import lt.marsrutai.mobile.android.dao.dto.VehiclePositionDto;
import lt.marsrutai.mobile.android.dao.exception.BackendException;
import lt.marsrutai.mobile.android.dao.exception.NoConnectivityException;
import lt.marsrutai.mobile.android.dao.exception.NoDataException;
import lt.marsrutai.mobile.android.exception.DataAccessException;
import lt.marsrutai.mobile.android.exception.UnableAccessDataException;
import lt.marsrutai.mobile.android.model.ArrivalTime;
import lt.marsrutai.mobile.android.model.DayGroup;
import lt.marsrutai.mobile.android.model.MobileScope;
import lt.marsrutai.mobile.android.model.Route;
import lt.marsrutai.mobile.android.model.RouteSearchResult;
import lt.marsrutai.mobile.android.model.RouteSearchStatus;
import lt.marsrutai.mobile.android.model.Schedule;
import lt.marsrutai.mobile.android.model.Segment;
import lt.marsrutai.mobile.android.model.SegmentPoint;
import lt.marsrutai.mobile.android.model.Stop;
import lt.marsrutai.mobile.android.model.StopArrivalTimes;
import lt.marsrutai.mobile.android.model.Track;
import lt.marsrutai.mobile.android.model.Transport;
import lt.marsrutai.mobile.android.model.VehiclePosition;
import lt.marsrutai.mobile.android.model.enums.DateTime;
import lt.marsrutai.mobile.android.model.enums.TrackType;
import lt.marsrutai.mobile.android.model.enums.TransportType;
import lt.marsrutai.mobile.android.model.enums.VehicleDirection;
import lt.marsrutai.mobile.android.model.enums.WeekdayType;
import lt.marsrutai.mobile.android.service.DataService;
import lt.marsrutai.mobile.android.util.StringUtils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Data service.
 *
 * @author Martynas Mickevicius
 *
 */
public class DataServiceImpl implements DataService {

    private static final Logger logger = LoggerFactory.getLogger("DataServiceImpl");

    private DataDao dao;

    public DataServiceImpl(DataDao dao) {
        this.dao = dao;
    }

    private <T> T invoke(ServiceBody<T> daoBody) throws DataAccessException {
        try {
            return daoBody.invoke();
        }
        catch (NoDataException ex) {
            logger.info("No data received");
            throw new UnableAccessDataException(ex);
        }
        catch (NoConnectivityException ex) {
            logger.info("No connectivity");
            throw new UnableAccessDataException(ex);
        }
        catch (BackendException ex) {
            logger.info("Exception while getting data " + ex.getMessage());
            throw new DataAccessException(ex);
        }
    }

    private abstract class ServiceBody<T> {

        protected abstract T invoke() throws BackendException;
    }

    @Override
    public List<MobileScope> getMobileScopes() throws DataAccessException {
        return invoke(new ServiceBody<List<MobileScope>>() {

            @Override
            protected List<MobileScope> invoke() throws BackendException {
                return assembleMobileScopes(dao.getAvailableMobileScopes());
            }
        });
    }

    private List<MobileScope> assembleMobileScopes(List<ScopeInfoDto> dtos) {
        List<MobileScope> list = new ArrayList<MobileScope>();
        for (ScopeInfoDto dto : dtos) {
            MobileScope mobileScope = assembleMobileScope(dto);
            if (mobileScope.isVisible()) {
                list.add(mobileScope);
            }
        }
        return list;
    }

    private MobileScope assembleMobileScope(ScopeInfoDto dto) {
        MobileScope mobileScope = new MobileScope();
        mobileScope.setFolder(dto.getFolder());
        mobileScope.setName(dto.getName());
        mobileScope.setVisible(Boolean.parseBoolean(dto.getVisible()));
        mobileScope.setTransports(assembleTransports(dto.getTransports()));
        mobileScope.setStoredVersion(dto.getStoredVersion());
        return mobileScope;
    }

    @Override
    public MobileScope getScopeInfo(final String scopeName) throws DataAccessException {
        return invoke(new ServiceBody<MobileScope>() {

            @Override
            protected MobileScope invoke() throws BackendException {
                return assembleMobileScope(dao.getScopeInfo(scopeName));
            }
        });
    }

    private List<Transport> assembleTransports(List<TransportDto> dtos) {
        List<Transport> list = new ArrayList<Transport>();
        for (TransportDto dto : dtos) {
            list.add(assembleTransport(dto));
        }
        return list;
    }

    private Transport assembleTransport(TransportDto dto) {
        Transport transport = new Transport();
        transport.setId(dto.getId());
        transport.setType(TransportType.parseEnum(dto.getTransportType()));
        transport.setColor(dto.getColor());
        transport.setLocalizedName(dto.getLocalizedName());
        transport.setLocalizedNamePlural(dto.getLocalizedNamePlural());
        return transport;
    }

    @Override
    public List<String> getStopNames(final String scopeName) throws DataAccessException {
        return invoke(new ServiceBody<List<String>>() {

            @Override
            protected List<String> invoke() throws BackendException {
                return dao.getStopNames(scopeName);
            }
        });
    }

    @Override
    public List<Stop> getStops(final String scopeName, final boolean withSchedules, final String searchQuery, final Double lat,
            final Double lng, final Double distance) throws DataAccessException {
        return invoke(new ServiceBody<List<Stop>>() {

            @Override
            protected List<Stop> invoke() throws BackendException {
                return assembleStops(dao.getStops(scopeName, withSchedules, searchQuery, lat, lng, distance));
            }
        });
    }

    @Override
    public Stop getStopById(final String scopeName, final String stopId) throws DataAccessException {
        return invoke(new ServiceBody<Stop>() {

            @Override
            protected Stop invoke() throws BackendException {
                return assembleStop(dao.getStopById(scopeName, stopId));
            }
        });
    }

    @Override
    public List<ArrivalTime> getArrivalTimes(final String scopeName, final String stopId, final String stopFullName, final String scheduleId, final int maxMinutes,
            final int maxResults, final String time, final boolean isConnected) throws DataAccessException {
        final Map<String, TransportType> transportIdToTypeMap = assembleTransportIdToTypeMap(getMobileScopes());
        return invoke(new ServiceBody<List<ArrivalTime>>() {

            @Override
            protected List<ArrivalTime> invoke() throws BackendException {
                return assembleArrivalTimes(dao.getArrivalTimes(scopeName, stopId, stopFullName, scheduleId, maxMinutes, maxResults, time, isConnected),
                        transportIdToTypeMap);
            }
        });
    }

    @Override
    public List<Schedule> getSchedules(final String scopeName) throws DataAccessException {
        return invoke(new ServiceBody<List<Schedule>>() {

            @Override
            protected List<Schedule> invoke() throws BackendException {
                return assembleSchedules(dao.getSchedules(scopeName, null));
            }
        });
    }

    private List<Schedule> assembleSchedules(List<ScheduleDto> dtos) {
        List<Schedule> list = new ArrayList<Schedule>();
        for (ScheduleDto dto : dtos) {
            list.add(assembleSchedule(dto));
        }
        return list;
    }

    @Override
    public Schedule getScheduleById(final String scopeName, final String scheduleId) throws DataAccessException {
        return invoke(new ServiceBody<Schedule>() {

            @Override
            protected Schedule invoke() throws BackendException {
                return assembleSchedule(dao.getScheduleById(scopeName, scheduleId));
            }
        });
    }

    private Schedule assembleSchedule(ScheduleDto dto) {
        Schedule schedule = new Schedule();
        schedule.setId(dto.getId());
        schedule.setName(dto.getName());
        schedule.setTrackName(dto.getTrackName());
        schedule.setTransportType(TransportType.parseEnum(dto.getTransportType()));
        schedule.setTransportId(dto.getTransport());
        schedule.setValidComment(dto.getValidComment());
        return schedule;
    }

    @Override
    public List<Track> getTracks(final String scopeName, final String scheduleId) throws DataAccessException {
        return invoke(new ServiceBody<List<Track>>() {

            @Override
            protected List<Track> invoke() throws BackendException {
                return assembleTracks(dao.getTracks(scopeName, scheduleId));
            }
        });
    }

    private List<Track> assembleTracks(List<BusTrackDto> dtos) {
        List<Track> list = new ArrayList<Track>();
        for (BusTrackDto dto : dtos) {
            TrackType trackType = TrackType.parseEnum(dto.getTrackType());
            if (TrackType.NORMAL.equals(trackType) || TrackType.CHANGED.equals(trackType)) {
                list.add(assembleTrack(dto));
            }
        }
        return list;
    }

    @Override
    public Track getTrackById(final String scopeName, final String trackId) throws DataAccessException {
        return invoke(new ServiceBody<Track>() {

            @Override
            protected Track invoke() throws BackendException {
                return assembleTrack(dao.getTrackById(scopeName, trackId));
            }
        });
    }

    private Track assembleTrack(BusTrackDto dto) {
        Track track = new Track();
        track.setId(dto.getId());
        track.setName(dto.getName());
        track.setScheduleId(dto.getScheduleId());
        track.setTrackType(TrackType.parseEnum(dto.getTrackType()));
        return track;
    }

    @Override
    public List<Stop> getTrackStops(final String scopeName, final String trackId) throws DataAccessException {
        return invoke(new ServiceBody<List<Stop>>() {

            @Override
            protected List<Stop> invoke() throws BackendException {
                return assembleStops(dao.getTrackStops(scopeName, trackId));
            }
        });
    }

    private List<Stop> assembleStops(List<BusStopDto> dtos) {
        List<Stop> list = new ArrayList<Stop>();
        for (BusStopDto dto : dtos) {
            list.add(assembleStop(dto));
        }
        return list;
    }

    private Stop assembleStop(BusStopDto dto) {
        Stop stop = new Stop();
        stop.setId(dto.getId());
        stop.setName(dto.getName());
        stop.setFullName(dto.getFullName());
        stop.setDirectionName(dto.getDirectionName());
        stop.setLatitude(null == dto.getLat() ? null : new BigDecimal(dto.getLat()));
        stop.setLongitude(null == dto.getLng() ? null : new BigDecimal(dto.getLng()));
        stop.setSchedules(dto.getSchedules() != null ? assembleSchedules(dto.getSchedules()) : null);

        return stop;
    }

    @Override
    public List<DayGroup> getDifferentRunningDays(final String scopeName, final String scheduleId) throws DataAccessException {
        return invoke(new ServiceBody<List<DayGroup>>() {

            @Override
            protected List<DayGroup> invoke() throws BackendException {
                return assembleDaysOfWeek(dao.getDifferentRunningDays(scopeName, scheduleId));
            }
        });
    }

    private List<DayGroup> assembleDaysOfWeek(List<DaysOfWeekDto> dtos) {
        List<DayGroup> list = new ArrayList<DayGroup>();
        for (DaysOfWeekDto dto : dtos) {
            DayGroup daysOfWeek = new DayGroup(assembleWeekdays(dto.getDays()));
            list.add(daysOfWeek);
        }
        return list;
    }

    private Set<WeekdayType> assembleWeekdays(List<Integer> dtos) {
        Set<WeekdayType> set = EnumSet.noneOf(WeekdayType.class);
        for (Integer dto : dtos) {
            set.add(WeekdayType.parseEnumSunSat(dto));
        }
        return set;
    }

    @Override
    public StopArrivalTimes getStopTimes(final String scopeName, final String scheduleId, final String trackId, final String stopId) throws DataAccessException {

        return invoke(new ServiceBody<StopArrivalTimes>() {

            @Override
            protected StopArrivalTimes invoke() throws BackendException {
                List<StopDayTimesDto> stopDayTimes = dao.getStopDayTimes(scopeName, trackId, stopId);

                StopArrivalTimes stopArrivalTimes = new StopArrivalTimes();
                for (StopDayTimesDto arrivalTimesByDaysOfWeek : stopDayTimes) {
                    if (arrivalTimesByDaysOfWeek.getArrivalTimes() != null) {
                        DayGroup dayGroup = new DayGroup(arrivalTimesByDaysOfWeek.getDaysOfWeek().getDays());
                        stopArrivalTimes.addArrivalTimes(dayGroup, assembleArrivalTimes(arrivalTimesByDaysOfWeek.getArrivalTimes()));
                    }
                }

                return stopArrivalTimes;
            }
        });
    }

    private List<ArrivalTime> assembleArrivalTimes(List<ArrivalTimeDto> dtos) {
        return assembleArrivalTimes(dtos, null);
    }

    private List<ArrivalTime> assembleArrivalTimes(List<ArrivalTimeDto> dtos, Map<String, TransportType> transportIdToTypeMap) {
        List<ArrivalTime> list = new ArrayList<ArrivalTime>();
        for (ArrivalTimeDto dto : dtos) {
            ArrivalTime arrivalTime = new ArrivalTime();
            arrivalTime.setScheduledTime(new DateTime(dto.getScheduleTime()).getCalendar());
            arrivalTime.setExpectedTime(new DateTime(dto.getExpectedTime()).getCalendar());
            arrivalTime.setDirectionName(dto.getDirectionName());
            arrivalTime.setScheduleId(dto.getScheduleId());
            arrivalTime.setScheduleName(dto.getScheduleName());
            arrivalTime.setTrackId(dto.getTrackId());
            arrivalTime.setTrackType(TrackType.parseEnum(dto.getTrackType()));
            arrivalTime.setTransportId(dto.getTransport());
            arrivalTime.setStopDirectionName(dto.getStopDirectionName());
            arrivalTime.setRunId(dto.getRunId());
            arrivalTime.setStopName(dto.getStopName());
            arrivalTime.setStopId(dto.getStopId());
            arrivalTime.setIsAccessible(dto.getIsAccessible());
            if (null != transportIdToTypeMap) {
                arrivalTime.setTransportType(assembleTransportType(dto.getTransport(), transportIdToTypeMap));
            }
            list.add(arrivalTime);
        }
        return list;
    }

    @Override
    public RouteSearchResult findRoutes(final String scopeName, final String start, final String end, final String time,
            final boolean isArrival, final int maxRides, final int changeDuration, final int maxWalkDistance, final int walkSpeed,
            final String transports, final boolean withPolyline) throws DataAccessException {

        final Map<String, TransportType> transportIdToTypeMap = assembleTransportIdToTypeMap(getMobileScopes());

        return invoke(new ServiceBody<RouteSearchResult>() {

            @Override
            protected RouteSearchResult invoke() throws BackendException {
                return assembleRouteSearchResult(dao.findRoutes(scopeName, start, end, time, isArrival, maxRides, changeDuration, maxWalkDistance, walkSpeed, transports, withPolyline), transportIdToTypeMap);
            }
        });
    }

    private Map<String, TransportType> assembleTransportIdToTypeMap(List<MobileScope> mobileScopes) {
        Map<String, TransportType> map = new HashMap<String, TransportType>();
        for (MobileScope mobileScope : mobileScopes) {
            for (Transport transport : mobileScope.getTransports()) {
                map.put(transport.getId(), transport.getType());
            }
        }
        return map;
    }

    private RouteSearchResult assembleRouteSearchResult(SearchResultDto dto, Map<String, TransportType> transportIdToTypeMap) {
        RouteSearchResult result = new RouteSearchResult();
        result.setEndChoices(dto.getEndChoices());
        result.setStartChoices(dto.getStartChoices());
        result.setRoutes(assembleRoutes(dto.getRoutes(), transportIdToTypeMap));
        result.setEndStatus(parseRouteSearchStatus(dto.getEndStatus()));
        result.setStartStatus(parseRouteSearchStatus(dto.getStartStatus()));

        return result;
    }

    private RouteSearchStatus parseRouteSearchStatus(String startStatus) {
        if (StringUtils.equals(startStatus, "OK")) {
            return RouteSearchStatus.OK;
        }
        else if (StringUtils.equals(startStatus, "ClarifyName")) {
            return RouteSearchStatus.CLARIFY_NAME;
        }
        else if (StringUtils.equals(startStatus, "NameNotFound")) {
            return RouteSearchStatus.NAME_NOT_FOUND;
        }
        return null;
    }

    private List<Route> assembleRoutes(List<RouteDto> dtos, Map<String, TransportType> transportIdToTypeMap) {
        List<Route> list = new ArrayList<Route>();
        if (null != dtos) {
            for (RouteDto dto : dtos) {
                Route route = new Route();
                route.setSegments(assembleSegments(dto.getSegments(), transportIdToTypeMap));
                list.add(route);
            }
        }
        return list;
    }

    private List<Segment> assembleSegments(List<SegmentDto> dtos, Map<String, TransportType> transportIdToTypeMap) {
        List<Segment> list = new ArrayList<Segment>();
        for (SegmentDto dto : dtos) {
            Segment segment = new Segment();
            segment.setEndName(dto.getEndName());
            segment.setEndTime(new DateTime(dto.getEndTime()).getCalendar());

            segment.setScheduleId(dto.getScheduleId());
            segment.setScheduleName(dto.getScheduleName());

            segment.setRunId(dto.getRunId());
            segment.setTrackid(dto.getTrackid());

            segment.setStartName(dto.getStartName());
            segment.setStartTime(new DateTime(dto.getStartTime()).getCalendar());

            segment.setTransportType(assembleTransportType(dto.getTransport(), transportIdToTypeMap));
            segment.setTransportId(dto.getTransport());

            BusStopDto startStop = dto.getStartStop();
            segment.setStartStop(startStop != null ? assembleStop(startStop) : null);

            BusStopDto endStop = dto.getEndStop();
            segment.setEndStop(endStop != null ? assembleStop(endStop) : null);

            segment.setGeoPoints(assembleSegmentPoints(dto.getGeoPoints()));
            list.add(segment);
        }
        return list;
    }

    private List<SegmentPoint> assembleSegmentPoints(List<GeoPointDto> geoPoints) {
        List<SegmentPoint> list = new ArrayList<SegmentPoint>();

        if (geoPoints != null) {
            for (GeoPointDto geoPointDto : geoPoints) {
                SegmentPoint geoPoint = new SegmentPoint();
                geoPoint.setLatitude(geoPointDto.getLat() == null ? null : new BigDecimal(geoPointDto.getLat()));
                geoPoint.setLongitude(geoPointDto.getLng() == null ? null : new BigDecimal(geoPointDto.getLng()));
                geoPoint.setStopId(geoPointDto.getStopId());
                geoPoint.setStopName(geoPointDto.getStopName());
                geoPoint.setStopDirectionName(geoPointDto.getStopDirectionName());

                if (geoPointDto.getTime() != null) {
                    geoPoint.setTime(new DateTime(geoPointDto.getTime()).getCalendar());
                }

                list.add(geoPoint);
            }
        }

        return list;
    }

    private TransportType assembleTransportType(String transportId, Map<String, TransportType> transportIdToTypeMap) {
        if (transportIdToTypeMap.containsKey(transportId)) {
            return transportIdToTypeMap.get(transportId);
        }
        else {
            return TransportType.WALK;
        }
    }

    @Override
    public List<Track> getTracksWithIds(String scopeName, String[] trackIds) throws DataAccessException {
        List<Track> list = new ArrayList<Track>();
        for (String trackId : trackIds) {
            list.add(getTrackById(scopeName, trackId));
        }
        return list;
    }

    @Override
    public List<ArrivalTime> getRunStops(final String scopeName, final String runId) throws DataAccessException {
        return invoke(new ServiceBody<List<ArrivalTime>>() {
            @Override
            protected List<ArrivalTime> invoke() throws BackendException {
                return assembleArrivalTimes(dao.getRunById(scopeName, runId).getTimes());
            }
        });
    }

    @Override
    public List<Transport> getTransports(final String scopeName) throws DataAccessException {
        return invoke(new ServiceBody<List<Transport>>() {
            @Override
            protected List<Transport> invoke() throws BackendException {
                return assembleTransports(dao.getTransports(scopeName));
            }
        });
    }

    @Override
    public List<VehiclePosition> getVehiclePositions(final String scopeFolder, final String transportId, final String scheduleName) throws DataAccessException {
        return invoke(new ServiceBody<List<VehiclePosition>>() {

            @Override
            protected List<VehiclePosition> invoke() throws BackendException {
                return assembleVehiclePositions(dao.getVehiclePositions(scopeFolder, transportId, scheduleName), scopeFolder);
            }
        });
    }

    protected List<VehiclePosition> assembleVehiclePositions(List<VehiclePositionDto> vehiclePositionDtos, String scopeFolder) {
        List<VehiclePosition> result = new ArrayList<VehiclePosition>();

        if (vehiclePositionDtos != null) {
            for (VehiclePositionDto vehiclePositionDto : vehiclePositionDtos) {
                VehiclePosition vehiclePosition = new VehiclePosition();
                vehiclePosition.setDirection(VehicleDirection.parseEnum(vehiclePositionDto.getDirection()));
                vehiclePosition.setDirectionAngle(vehiclePositionDto.getDirectionAngle());
                vehiclePosition.setDirectionName(vehiclePositionDto.getDirectionName());
                vehiclePosition.setLatitude(vehiclePositionDto.getLat());
                vehiclePosition.setLongitude(vehiclePositionDto.getLng());
                vehiclePosition.setPositionTime(new DateTime(vehiclePositionDto.getPositionTime()).getCalendar());
                vehiclePosition.setScheduleName(vehiclePositionDto.getScheduleName());
                vehiclePosition.setTransportId(vehiclePositionDto.getTransport());
                vehiclePosition.setScopeFolder(scopeFolder);

                result.add(vehiclePosition);
            }
        }

        return result;
    }
}
